《Android 进阶(二)》 自定义View之Dota2能力雷达图

1. 前言

最近看Dota2的比赛的时候无意在一个应用中看到来一个能力分析的雷达图,就是展示你的各方面数据。你可能看见过这个图。

这里写图片描述

2. 实现思路

  1. 继承View,复写onDraw。
  2. 确定N边形和每个边对应的角度;
  3. 确定多边形外接圆的半径以及圆心(也就是中心点)
  4. 确定每条半径上的所有点的坐标。
  5. 确定每条数据在图形上的坐标;
  6. 确定文字在图形上的位置;
  7. 采用合适的绘制方式绘制;

3. 实现

3.1 定义自定义属性

主要定义这几个属性,可以根据需要继续扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<declare-styleable name="CustomRadarChart">
<!-- 蜘蛛网线条宽度 -->
<attr name="radarLineWidth" format="dimension"/>
<!-- 蜘蛛网颜色 -->
<attr name="radarLineColor" format="color"/>
<!-- 半径分成N段-->
<attr name="radarLineSegments" format="integer"/>
<!-- 文字颜色 -->
<attr name="radarTextColor" format="color"/>
<!-- 文字字体大小-->
<attr name="radarTextSize" format="integer"/>
<!-- 数据展示覆盖区域颜色 -->
<attr name="radarCoverColor" format="color"/>
</declare-styleable>

3.2 自定义View代码

注释代码中都有写出来,主要是看下实现的方式。没有过多的修饰和精简。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
public class CustomRadarChart extends View {

/**
* N 边形,默认为6边形
*/
private final int DEFAULT_PIECE_NUMBER = 7;

/**
* 线条宽度,默认为10px
*/
private final int DEFAULT_LINE_WIDTH = 4;

/**
* 线条颜色,默认为灰色
*/
private final int DEFAULT_LINE_COLOR = 0xffd0d6dc;

/**
* 半径分成N段,默认为4段,圆心算一段
*/
private final int DEFAULT_LINE_SEGMENTS = 4;

/**
* 外接圆半径,默认为50px
*/
private final int DEFAULT_RADIUS = 50;

/**
* 文本颜色和文本字体, 默认为黑色,10px
*/
private final int DEFAULT_TEXT_COLOR = 0xff647d91;
private final int DEFAULT_TEXT_SIZE = 10;

/**
* 覆盖面绘制颜色
*/
private final int DEFAULT_COVER_COLOR = 0x55ced6dc;

private int mPieceNumber = DEFAULT_PIECE_NUMBER;
private int mRadius = DEFAULT_RADIUS;

private int mLineWidth = DEFAULT_LINE_WIDTH;
private int mLineColor = DEFAULT_LINE_COLOR;
private int mLineSegments = DEFAULT_LINE_SEGMENTS;
private int mTextColor = DEFAULT_TEXT_COLOR;
private int mTextSize = DEFAULT_TEXT_SIZE;
private int mCoverColor = DEFAULT_COVER_COLOR;

private double mAverageAngle = 0;

private Paint mRadarPaint;
private TextPaint mTextPaint;
private Paint mCoverPaint;
private Path mCoverPath;

List<RadarPoints> mRadarPointses = new ArrayList<>();
List<RadarEntry> mRadarEntries = new ArrayList<>();
List<PointF> mCoverPoints = new ArrayList<>();
List<PointF> mTextPoints = new ArrayList<>();

/**
* 外接圆中心位置
*/
private int mPositionX = 0;
private int mPositionY = 0;

public CustomRadarChart(Context context) {
this(context, null);
}

public CustomRadarChart(Context context,
@Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public CustomRadarChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray attributes = getContext().obtainStyledAttributes(attrs, R.styleable.CustomRadarChart);

mLineWidth = (int) attributes.getDimension(R.styleable.CustomRadarChart_radarLineWidth, DEFAULT_LINE_WIDTH);
mLineColor = attributes.getColor(R.styleable.CustomRadarChart_radarLineColor, DEFAULT_LINE_COLOR);
mLineSegments = attributes.getInteger(R.styleable.CustomRadarChart_radarLineSegments, DEFAULT_LINE_SEGMENTS);
mTextColor = attributes.getColor(R.styleable.CustomRadarChart_radarTextColor, DEFAULT_TEXT_COLOR);

mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
attributes.getInteger(R.styleable.CustomRadarChart_radarTextSize, DEFAULT_TEXT_SIZE), getResources().getDisplayMetrics());
mCoverColor = (int) attributes.getColor(R.styleable.CustomRadarChart_radarCoverColor, DEFAULT_COVER_COLOR);

init();
}

private void init() {
/**
* 蜘蛛网Paint初始化
*/
mRadarPaint = new Paint();
mRadarPaint.setColor(mLineColor);
mRadarPaint.setStrokeWidth(mLineWidth);
mRadarPaint.setAntiAlias(true);
mRadarPaint.setStyle(Paint.Style.STROKE);

/**
* 文字绘制Paint初始化
*/
mTextPaint = new TextPaint();
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setAntiAlias(true);
mTextPaint.setStyle(Paint.Style.STROKE);

/**
*覆盖面绘制Paint初始化
*/
mCoverPaint = new Paint();
mCoverPaint.setColor(mCoverColor);
mCoverPaint.setAntiAlias(true);
mCoverPaint.setStyle(Paint.Style.FILL);
mCoverPath = new Path();
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mPositionX = w / 2;
mPositionY = h / 2;
mAverageAngle = 360.0 / mPieceNumber;

int max = 0;
for(RadarEntry entry : mRadarEntries) {
Rect textBound = new Rect();
mTextPaint.getTextBounds(entry.title, 0, entry.title.length(),
textBound);
max = Math.max(textBound.width(), max);
}
mRadius = Math.min(w / 2 - max, h / 2);

if (mRadarEntries==null || mRadarEntries.size()==0) {
throw new NullPointerException("请先设置数据集");
}
/**
* 计算每一条轴线上的所有点
*/
for (int i = 0; i < mPieceNumber; i++) {
List<PointF> pointFs = new ArrayList<>();
for (int j = 0; j < mLineSegments; j++) {
PointF point = new PointF();
double percent = j * 1.0 / (mLineSegments - 1);
point.set(getPloygonX(mAverageAngle * i, percent),
getPloygonY(mAverageAngle * i, percent));
pointFs.add(point);
}
RadarPoints radarPoints = new RadarPoints(i, pointFs);
mRadarPointses.add(radarPoints);
}

/**
* 根据数据集计算覆盖多变形的点
*/
for (int m = 0; m < mPieceNumber; m++) {
PointF pointF = new PointF();
double percent = mRadarEntries.get(m).level / 100.0;
pointF.set(getPloygonX(mAverageAngle * m, percent),
getPloygonY(mAverageAngle * m, percent));
mCoverPoints.add(pointF);
}

/**
* 设置文字显示位置
*/
for (int m = 0; m < mPieceNumber; m++) {
PointF pointF = new PointF();
String title = mRadarEntries.get(m).title;
Rect textBound = new Rect();
mTextPaint.getTextBounds(title, 0, title.length(),
textBound);
float boundx = mRadarPointses.get(m).getPointFs().get(mLineSegments -1).x;
float boundy = mRadarPointses.get(m).getPointFs().get(mLineSegments -1).y;
if( boundx > mRadius && boundy <= mRadius) {
pointF.set(getPloygonX(mAverageAngle * m, 1),
getPloygonY(mAverageAngle * m, 1) - textBound.height()*2) ;
} else if ( boundx <= mRadius && boundy <= mRadius){
pointF.set(getPloygonX(mAverageAngle * m, 1) - textBound.width(),
getPloygonY(mAverageAngle * m, 1) - textBound.height()*2);
} else if( boundx <= mRadius && boundy > mRadius) {
pointF.set(getPloygonX(mAverageAngle * m, 1) - textBound.width(),
getPloygonY(mAverageAngle * m, 1) );
} else {
pointF.set(getPloygonX(mAverageAngle * m, 1),
getPloygonY(mAverageAngle * m, 1));
}
mTextPoints.add(pointF);
}
}

/**
* 设置数据集,数据集的index决定位置,顺时针方向,起始角度为0度
*/
public void setRadatEntries(List<RadarEntry> entries) {
this.mRadarEntries = entries;
mPieceNumber = entries.size();
postInvalidate();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 绘制中心点
*/
canvas.drawPoint(mPositionX, mPositionY, mRadarPaint);

/**
* 绘制蜘蛛网
*/
for (int i = 0; i < mLineSegments; i++) {
for (int j = 0; j < mPieceNumber - 1; j++) {
canvas.drawLine(mRadarPointses.get(j).getPointFs().get(i).x, mRadarPointses.get(
j).getPointFs().get(i).y,
mRadarPointses.get(j + 1).getPointFs().get(i).x, mRadarPointses.get(
j + 1).getPointFs().get(i).y, mRadarPaint);
}
canvas.drawLine(mRadarPointses.get(mPieceNumber - 2).getPointFs().get(i).x,
mRadarPointses.get(mPieceNumber - 2).getPointFs().get(i).y,
mRadarPointses.get(mPieceNumber - 1).getPointFs().get(i).x, mRadarPointses.get(
mPieceNumber - 1).getPointFs().get(i).y, mRadarPaint);

canvas.drawLine(mRadarPointses.get(mPieceNumber - 1).getPointFs().get(i).x,
mRadarPointses.get(mPieceNumber - 1).getPointFs().get(i).y,
mRadarPointses.get(0).getPointFs().get(i).x, mRadarPointses.get(
0).getPointFs().get(i).y, mRadarPaint);
}

/**
* 绘制轴线
*/
for (int k = 0; k < mPieceNumber; k++) {
canvas.drawLine(mRadarPointses.get(k).getPointFs().get(0).x,
mRadarPointses.get(k).getPointFs().get(0).y,
mRadarPointses.get(k).getPointFs().get(mLineSegments - 1).x, mRadarPointses.get(
k).getPointFs().get(mLineSegments - 1).y, mRadarPaint);
}

/**
* 绘制数据
*/
if (mCoverPoints != null && mCoverPoints.size() == mPieceNumber) {
mCoverPath.reset();
mCoverPath.moveTo(mCoverPoints.get(0).x, mCoverPoints.get(0).y);
for (int i = 1; i < mPieceNumber; i++) {
mCoverPath.lineTo(mCoverPoints.get(i).x, mCoverPoints.get(i).y);
}
mCoverPath.close();
canvas.drawPath(mCoverPath, mCoverPaint);
} else {
throw new NullPointerException("请先设置数据集");
}

/**
* 绘制文字,使用StaticLayout进行换行文字的绘制
*/
for (int i = 0; i < mPieceNumber; i++) {
canvas.save();
String str= mRadarEntries.get(i).title + "\r\n" + Math.floor(mRadarEntries.get(i).level*10)/10;
StaticLayout layout = new StaticLayout(str, mTextPaint, 300,
Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, true);
canvas.translate(mTextPoints.get(i).x,mTextPoints.get(i).y);
layout.draw(canvas);
canvas.restore();
}

}

public float getPloygonX(double angle, double percent) {
return Float.parseFloat(
String.valueOf(
mPositionX + Math.cos(angle / 360.0 * 2 * Math.PI) * mRadius * percent));
}

public float getPloygonY(double angle, double percent) {
return Float.parseFloat(String.valueOf(
mPositionY + Math.sin(angle / 360.0 * 2 * Math.PI) * mRadius * percent));
}

/**
* 雷达图数据载体
*/
public static class RadarEntry {
private String title;
private Float level;

public RadarEntry(String title, float level) {
this.title = title;
this.level = level;
}
}

/**
* 每一条线上的所有点集合
*/
public class RadarPoints {
int index;
List<PointF> mPointFs;

public RadarPoints(int index, List<PointF> pointFs) {
this.index = index;
mPointFs = pointFs;
}

public int getIndex() {
return index;
}

public void setIndex(int index) {
this.index = index;
}

public List<PointF> getPointFs() {
return mPointFs;
}

public void setPointFs(List<PointF> pointFs) {
mPointFs = pointFs;
}
}

}

可能画图过程中的实现方式优点粗糙,大家可以根据自己的方式,改写即可。
个人是先将每条半径上的端点全部计算出来进行保存,然后一次连接,然后把每个半径首尾端点连接,然后绘制覆盖区域,覆盖区域也是采用先计算点位置,然后连接的方式实现,最后确定文本的显示位置和文字的绘制。由于这里涉及到绘制换行文字,使用到来StaticLayout。使用方法请查阅对应API。

4. 效果

这里写图片描述

5. 源码

源码依然是上传到Github
CustomViewDemo

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×